package ace.cmp.jetcache;

import com.alicp.jetcache.*;
import com.alicp.jetcache.anno.CacheConsts;
import com.alicp.jetcache.anno.CacheType;
import com.alicp.jetcache.embedded.EmbeddedCacheBuilder;
import com.alicp.jetcache.external.ExternalCacheBuilder;
import com.alicp.jetcache.support.BroadcastManager;
import com.alicp.jetcache.template.CacheBuilderTemplate;
import com.alicp.jetcache.template.CacheMonitorInstaller;
import com.alicp.jetcache.template.QuickConfig;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import javax.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author caspar
 * @date 2023/3/9 15:47 扩展 {@link SimpleCacheManager}
 * 1.不存在area的时候，使用default配置。（原：不存在area的时候，抛出异常。）
 */
public class AceSimpleCacheManager implements CacheManager, AutoCloseable {

  private static final boolean DEFAULT_CACHE_NULL_VALUE = false;

  private static final Logger logger = LoggerFactory.getLogger(SimpleCacheManager.class);

  // area -> cacheName -> Cache
  private final ConcurrentHashMap<String, ConcurrentHashMap<String, Cache>> caches =
      new ConcurrentHashMap<>();

  private final ConcurrentHashMap<String, BroadcastManager> broadcastManagers =
      new ConcurrentHashMap();

  private CacheBuilderTemplate cacheBuilderTemplate;

  public AceSimpleCacheManager() {}

  @PreDestroy
  @Override
  public void close() {
    broadcastManagers.forEach(
        (area, bm) -> {
          try {
            bm.close();
          } catch (Exception e) {
            logger.error("error during close broadcast manager", e);
          }
        });
    broadcastManagers.clear();
    caches.forEach(
        (area, areaMap) -> {
          areaMap.forEach(
              (cacheName, cache) -> {
                try {
                  cache.close();
                } catch (Exception e) {
                  logger.error("error during close Cache", e);
                }
              });
        });
    caches.clear();
  }

  private ConcurrentHashMap<String, Cache> getCachesByArea(String area) {
    return caches.computeIfAbsent(area, (key) -> new ConcurrentHashMap<>());
  }

  @Override
  public Cache getCache(String area, String cacheName) {
    ConcurrentHashMap<String, Cache> areaMap = getCachesByArea(area);
    return areaMap.get(cacheName);
  }

  @Override
  public void putCache(String area, String cacheName, Cache cache) {
    ConcurrentHashMap<String, Cache> areaMap = getCachesByArea(area);
    areaMap.put(cacheName, cache);
  }

  @Override
  public BroadcastManager getBroadcastManager(String area) {
    return broadcastManagers.get(area);
  }

  @Override
  public void putBroadcastManager(String area, BroadcastManager broadcastManager) {
    broadcastManagers.put(area, broadcastManager);
  }

  public CacheBuilderTemplate getCacheBuilderTemplate() {
    return cacheBuilderTemplate;
  }

  public void setCacheBuilderTemplate(CacheBuilderTemplate cacheBuilderTemplate) {
    this.cacheBuilderTemplate = cacheBuilderTemplate;
  }

  @Override
  public <K, V> Cache<K, V> getOrCreateCache(QuickConfig config) {
    if (cacheBuilderTemplate == null) {
      throw new IllegalStateException("cacheBuilderTemplate not set");
    }
    Objects.requireNonNull(config.getArea());
    Objects.requireNonNull(config.getName());
    ConcurrentHashMap<String, Cache> m = getCachesByArea(config.getArea());
    Cache c = m.get(config.getName());
    if (c != null) {
      return c;
    }
    return m.computeIfAbsent(config.getName(), n -> create(config));
  }

  private Cache create(QuickConfig config) {
    Cache cache;
    if (config.getCacheType() == CacheType.REMOTE) {
      cache = buildRemote(config);
    } else if (config.getCacheType() == CacheType.LOCAL) {
      cache = buildLocal(config);
    } else {
      Cache local = buildLocal(config);
      Cache remote = buildRemote(config);

      boolean useExpireOfSubCache = config.getLocalExpire() != null;
      cache =
          MultiLevelCacheBuilder.createMultiLevelCacheBuilder()
              .expireAfterWrite(
                  remote.config().getExpireAfterWriteInMillis(), TimeUnit.MILLISECONDS)
              .addCache(local, remote)
              .useExpireOfSubCache(useExpireOfSubCache)
              .cacheNullValue(
                  config.getCacheNullValue() != null
                      ? config.getCacheNullValue()
                      : DEFAULT_CACHE_NULL_VALUE)
              .buildCache();
    }
    if (config.getRefreshPolicy() != null) {
      cache = new RefreshCache(cache);
    } else if (config.getLoader() != null) {
      cache = new LoadingCache(cache);
    }
    cache.config().setRefreshPolicy(config.getRefreshPolicy());
    cache.config().setLoader(config.getLoader());

    boolean protect =
        config.getPenetrationProtect() != null
            ? config.getPenetrationProtect()
            : cacheBuilderTemplate.isPenetrationProtect();
    cache.config().setCachePenetrationProtect(protect);
    cache.config().setPenetrationProtectTimeout(config.getPenetrationProtectTimeout());

    for (CacheMonitorInstaller i : cacheBuilderTemplate.getCacheMonitorInstallers()) {
      i.addMonitors(this, cache, config);
    }
    return cache;
  }

  private Cache buildRemote(QuickConfig config) {
    ExternalCacheBuilder cacheBuilder =
        (ExternalCacheBuilder) cacheBuilderTemplate.getCacheBuilder(1, config.getArea());

    if (cacheBuilder == null) {
      cacheBuilder =
          (ExternalCacheBuilder) cacheBuilderTemplate.getCacheBuilder(1, CacheConsts.DEFAULT_AREA);
    }

    if (cacheBuilder == null) {
      throw new CacheConfigException("no remote cache builder: " + config.getArea());
    }

    if (config.getExpire() != null && config.getExpire().toMillis() > 0) {
      cacheBuilder.expireAfterWrite(config.getExpire().toMillis(), TimeUnit.MILLISECONDS);
    }

    String prefix;
    if (config.getUseAreaInPrefix() != null && config.getUseAreaInPrefix()) {
      prefix = config.getArea() + "_" + config.getName();
    } else {
      prefix = config.getName();
    }
    if (cacheBuilder.getConfig().getKeyPrefixSupplier() != null) {
      Supplier<String> supplier = cacheBuilder.getConfig().getKeyPrefixSupplier();
      cacheBuilder.setKeyPrefixSupplier(() -> supplier.get() + prefix);
    } else {
      cacheBuilder.setKeyPrefix(prefix);
    }

    if (config.getKeyConvertor() != null) {
      cacheBuilder.getConfig().setKeyConvertor(config.getKeyConvertor());
    }
    if (config.getValueEncoder() != null) {
      cacheBuilder.getConfig().setValueEncoder(config.getValueEncoder());
    }
    if (config.getValueDecoder() != null) {
      cacheBuilder.getConfig().setValueDecoder(config.getValueDecoder());
    }

    cacheBuilder.setCacheNullValue(
        config.getCacheNullValue() != null ? config.getCacheNullValue() : DEFAULT_CACHE_NULL_VALUE);
    return cacheBuilder.buildCache();
  }

  private Cache buildLocal(QuickConfig config) {
    EmbeddedCacheBuilder cacheBuilder =
        (EmbeddedCacheBuilder) cacheBuilderTemplate.getCacheBuilder(0, config.getArea());

    if (cacheBuilder == null) {
      cacheBuilder =
          (EmbeddedCacheBuilder) cacheBuilderTemplate.getCacheBuilder(0, CacheConsts.DEFAULT_AREA);
    }

    if (cacheBuilder == null) {
      throw new CacheConfigException("no local cache builder: " + config.getArea());
    }

    if (config.getLocalLimit() != null && config.getLocalLimit() > 0) {
      cacheBuilder.setLimit(config.getLocalLimit());
    }
    if (config.getCacheType() == CacheType.BOTH
        && config.getLocalExpire() != null
        && config.getLocalExpire().toMillis() > 0) {
      cacheBuilder.expireAfterWrite(config.getLocalExpire().toMillis(), TimeUnit.MILLISECONDS);
    } else if (config.getExpire() != null && config.getExpire().toMillis() > 0) {
      cacheBuilder.expireAfterWrite(config.getExpire().toMillis(), TimeUnit.MILLISECONDS);
    }
    if (config.getKeyConvertor() != null) {
      cacheBuilder.getConfig().setKeyConvertor(config.getKeyConvertor());
    }
    cacheBuilder.setCacheNullValue(
        config.getCacheNullValue() != null ? config.getCacheNullValue() : DEFAULT_CACHE_NULL_VALUE);
    return cacheBuilder.buildCache();
  }
}
